﻿using System.Windows.Media.Imaging;
using DeepEarth.Map.Core;
using System.Collections.Generic;
using DeepEarth.Toolkit.Geometry.Data;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using DeepEarth.Core;
using DeepEarth.Map.Core.Utilities;
using System;
using DeepEarth.Toolkit.Geometry.Clipping;
namespace DeepEarth.Toolkit.Geometry
{
    public class GeometryRenderer
    {
        WriteableBitmap LeftBuffer { get; set; }
        WriteableBitmap RightBuffer { get; set; }

        WriteableBitmap wb;
        public WriteableBitmap WriteableBitmap
        {
            get { return wb; }
            set
            {
                wb = value;
            }
        }

        IMap map;
        public IMap MapInstance
        {
            get { return map; }
            set
            {
                map = value;
            }
        }

        List<Data.ShapeGeometry> geometries;
        public List<Data.ShapeGeometry> Geometries
        {
            get 
            {
                if (geometries == null)
                    geometries = new List<ShapeGeometry>();

                return geometries;
            }
            set
            {
                geometries = value;
            }
        }

        public void Refresh(double maxBound)
        {
            if (MapInstance == null || WriteableBitmap == null)
            {
                return;
            }
            
            WriteableBitmap.Clear();

            if (!Geometries.Any())
                return;

            CalculateOffsets();

            foreach (var geo in Geometries)
            {
                if (geo is LineStringGeometry)
                {
                    DrawLine((LineStringGeometry)geo, maxBound);
                }
                else
                {
                    DrawPoly((PolygonGeometry)geo, maxBound);
                }
            }

            if (UseBuffer)
            {
                if (EntireWorldVisible)
                {
                    Point offsetLoc = new Point(-OffsetX, -OffsetY);
                    WriteableBitmap.Blit(new Rect(offsetLoc, new Size(MapWidth - 1, MapWidth)), LeftBuffer, new Rect(0, 0, MapWidth, MapWidth));

                    int clonesRequired = (int)Math.Ceiling(MapLogicalWidth);

                    // Entireworldvisible = map is wrapped AND logical width > 1. We need to replicate the buffer (The first buffer draws
                    // over the left visible map.
                    for (int i = 1; i <= clonesRequired; i++)
                    {
                        Point right = new Point(-OffsetX + (MapWidth * i), -OffsetY);
                        WriteableBitmap.Blit(new Rect(right, new Size(MapWidth, MapWidth)), LeftBuffer, new Rect(0, 0, MapWidth, MapWidth));  
                    } 
                }
                else
                {
                    WriteableBitmap.Blit(new Rect(0, 0, MapViewportPixelWidth, MapViewportPixelHeight), LeftBuffer, new Rect(0, 0, MapViewportPixelWidth, MapViewportPixelHeight));

                    // Add 1 as both buffers have pixels for the -180 or 180 location and we don't want to double up
                    // Also there seem to be performance advantages if we don't blit over the same destination points.
                    // Keep the blit as tight as possible otherwise there is a sharp performance degradation
                    double rightOffset = -LeftBufferOffsetX + MapWidth + 1;
                    WriteableBitmap.Blit(new Rect(rightOffset, 0, MapViewportPixelWidth, MapViewportPixelHeight), RightBuffer, new Rect(rightOffset, 0, MapViewportPixelWidth, MapViewportPixelHeight));
                }    
            }

            WriteableBitmap.Invalidate();
        }

        double OffsetX {get;set;}
        double OffsetY { get; set; }
        double MapWidth { get; set; }
        double MapLogicalWidth { get; set; }
        double MapViewportPixelWidth { get; set; }
        double MapViewportPixelHeight { get; set; }

        bool UseBuffer { get; set; }
        double LeftBufferOffsetX { get; set; }
        double RightBufferOffsetX { get; set; }
        bool EntireWorldVisible { get; set; }

        // find the map pixel position of the top left corner of the screen
        void CalculateOffsets()
        {
            MapLogicalWidth = MapInstance.CurrentViewportLogicalWidth;

            MapViewportPixelWidth = MapInstance.AsFrameworkElement.ActualWidth;
            MapViewportPixelHeight = MapInstance.AsFrameworkElement.ActualHeight;
            MapWidth = MapViewportPixelWidth / MapLogicalWidth;

            Location topLeft = MapInstance.ViewportPointToLocation(new Point(0,0));

            Point offset = CoordinateTransformation.GeographicToMapPixel(topLeft, MapWidth);
            OffsetX = offset.X;
            OffsetY = offset.Y;


            if (MapInstance.IsWrapped)
            {
                // If we're zoomed right out, need to offset the offset to account for the 
                // the top left corner of the world being somewhere in the middle of the screen
                var zoomedOutYOffset = CoordinateTransformation.FindYOffset(MapInstance);
                OffsetY -= zoomedOutYOffset.Y;
            }

            EntireWorldVisible = false;
            UseBuffer = false;

            RightBuffer = null;

            if (MapInstance.IsWrapped)
            {
                if (MapLogicalWidth >= 1)
                {
                    EntireWorldVisible = true;
                    UseBuffer = true;                                      
                    LeftBuffer = new WriteableBitmap((int)MapWidth, (int)MapWidth);
                }
                else
                {
                    Point logicalOrigin = CoordinateTransformation.GeographicToLogical(MapInstance.CurrentCenter);
                    double halfLogicalView = MapLogicalWidth / 2d;

                    bool datelineVisible = 
                        logicalOrigin.X - halfLogicalView <= 0 ||
                        logicalOrigin.X + halfLogicalView  >= 1;

                    if (datelineVisible)
                    {
                        UseBuffer = true;
                        LeftBuffer = new WriteableBitmap((int)MapViewportPixelWidth, (int)MapViewportPixelHeight);
                        RightBuffer = new WriteableBitmap((int)MapViewportPixelWidth, (int)MapViewportPixelHeight);

                        LeftBufferOffsetX = OffsetX;
                        RightBufferOffsetX = OffsetX - MapWidth;
                    }
                }
            }
        }


        void DrawPoly(PolygonGeometry ml, double maxBound)
        {
            bool outline = false;

            SolidColorBrush stroke = ml.Stroke as SolidColorBrush;
            Color strokeColor = Colors.Transparent;
            double strokeThickness;
            if (stroke != null)
            {
                strokeColor = stroke.Color;
                strokeThickness = ml.StrokeThickness;
                outline = strokeThickness > 0;
            }

            SolidColorBrush fillBrush = ml.Fill as SolidColorBrush;

            Color fillColor = Colors.Transparent;
            bool fill = false;
            if (fillBrush != null)
            {
                fillColor = fillBrush.Color;
                fill = true;
            }
            else if (!outline)
            {
                // no fill and no outline - don't draw
                return;
            }

            var points = ml.Locations.Select(l => CoordinateTransformation.GeographicToMapPixel(l, MapWidth)).ToList();

            Point first = points.First();

            if (first != points.Last())
            {
                points.Add(new Point(first.X, first.Y));
            }

            if (UseBuffer)
            {
                Tuple<int[], int[]> pointArrays = OffsetPoints(points);
                if (fill)
                    LeftBuffer.FillPolygon(pointArrays.Item1, fillColor);

                if (outline)
                    LeftBuffer.DrawPolyline(pointArrays.Item1, strokeColor);
 
                if (RightBuffer != null)
                {
                    if (fill)
                        RightBuffer.FillPolygon(pointArrays.Item2, fillColor);

                    if (outline)
                        RightBuffer.DrawPolyline(pointArrays.Item2, strokeColor);
                }
            }
            else
            {
                if (fill)
                {
                    // TODO : Create clipped polygons
                    Tuple<int[], int[]> pointArrays = OffsetPointsWithPolygonClipping(points, maxBound);
                    WriteableBitmap.FillPolygon(pointArrays.Item1, fillColor);
                }

                if (outline)
                {
                    List<Tuple<int[], int[]>> clipped = OffsetPointsWithLineClipping(points, maxBound);

                    foreach (var clip in clipped)
                        WriteableBitmap.DrawPolyline(clip.Item1, strokeColor);
                }
            }
        }

        void DrawLine(LineStringGeometry ml, double maxBound)
        {
            SolidColorBrush stroke = ml.Stroke as SolidColorBrush;
            Color strokeColor = Colors.Transparent;
            double strokeThickness = 0;
            if (stroke != null)
            {
                strokeColor = stroke.Color;
                strokeThickness = ml.StrokeThickness;
            }

            if (stroke == null || strokeThickness == 0)
            {
                return;
            }

            var points = ml.Locations.Select(l => CoordinateTransformation.GeographicToMapPixel(l, MapWidth)).ToList();

            if (UseBuffer)
            {
                Tuple<int[], int[]> pointArrays = OffsetPoints(points);
                LeftBuffer.DrawPolyline(pointArrays.Item1, strokeColor);

                if (RightBuffer != null)
                    RightBuffer.DrawPolyline(pointArrays.Item2, strokeColor);

            }
            else
            {
                // need to clip otherwise performance goes to hell as we attempt to draw lines with endpoints way outside the viewport
                List<Tuple<int[], int[]>> pointArrays = OffsetPointsWithLineClipping(points, maxBound);
                foreach (var clipped in pointArrays)
                {
                    WriteableBitmap.DrawPolyline(clipped.Item1, strokeColor);
                }
            }


        }

        Tuple<int[], int[]> OffsetPoints(List<Point> points)
        {         
            if (UseBuffer)
            {
                return OffsetPointsForBuffers(points);
            }
            else
            {
                var pointArray = new int[points.Count * 2];
                int index = 0;
                foreach (Point p in points)
                {
                    pointArray[index++] = (int)(p.X - OffsetX);
                    pointArray[index++] = (int)(p.Y - OffsetY);
                }

                return new Tuple<int[], int[]>(pointArray, null);
            }
        }

        List<Tuple<int[], int[]>> OffsetPointsWithLineClipping(List<Point> points, double maxBound)
        {
            List<Tuple<int[], int[]>> retVal = new List<Tuple<int[],int[]>>();
            if (UseBuffer)
            {               
                retVal.Add(OffsetPointsForBuffers(points));
                return retVal;                
            }
            else
            {
                for(int i=0;i<points.Count; i++)
                {
                    Point current = points[i];
                    current.X -= OffsetX;
                    current.Y -= OffsetY;
                    points[i] = current;
                }

                var clippedPoints = LineClipper.Clip(points, maxBound);

                foreach (var pts in clippedPoints)
                {
                    var pointArray = new int[pts.Count * 2];
                    int index = 0;
                    foreach (Point p in pts)
                    {
                        pointArray[index++] = (int)(p.X);
                        pointArray[index++] = (int)(p.Y);
                    }

                    retVal.Add(new Tuple<int[], int[]>(pointArray, null));
                }

                return retVal;
            }
        }

        Tuple<int[], int[]> OffsetPointsWithPolygonClipping(List<Point> points, double maxBound)
        {
            if (UseBuffer)
            {
                return OffsetPointsForBuffers(points);                
            }
            else
            {
                for (int i = 0; i < points.Count; i++)
                {
                    Point current = points[i];
                    current.X -= OffsetX;
                    current.Y -= OffsetY;
                    points[i] = current;
                }

                var clippedPoints = SimplifedSutherlandHodgman.BoxClip(points, maxBound);

                var pointArray = new int[clippedPoints.Count * 2];
                    int index = 0;
                    foreach (Point p in clippedPoints)
                    {
                        pointArray[index++] = (int)(p.X);
                        pointArray[index++] = (int)(p.Y);
                    }

                return new Tuple<int[], int[]>(pointArray, null);
            }
        }

        Tuple<int[], int[]> OffsetPointsForBuffers(List<Point> points)
        {
            System.Diagnostics.Debug.Assert(UseBuffer);

            var pointArray = new int[points.Count * 2];

            int index = 0;
            if (EntireWorldVisible)
            {
                // The buffer that we're using covers the world with no regard to the position of the map
                // on the screen. We will blit it onto the correct position later
                foreach (Point p in points)
                {
                    pointArray[index++] = (int)(p.X);
                    pointArray[index++] = (int)(p.Y);
                }

                return new Tuple<int[], int[]>(pointArray, null);
            }
            else
            {
                var rightPointArray = new int[points.Count * 2];

                foreach (Point p in points)
                {
                    pointArray[index] = (int)(p.X - LeftBufferOffsetX);
                    rightPointArray[index++] = (int)(p.X - RightBufferOffsetX);
                    pointArray[index] = (int)(p.Y - OffsetY);
                    rightPointArray[index++] = (int)(p.Y - OffsetY);
                }
                return new Tuple<int[], int[]>(pointArray, rightPointArray);
            }
        }


        // TODO : Handle geos that cross the dateline using something like the commented out code below

        //void DrawPoly(PolygonGeometry pg, bool handleWrap)
        //{
        //    double wrapPoint = 180;

        //    if (handleWrap && wrapPoint > -180 && MapInstance.IsWrapped)
        //    {
        //        var simples = SimplePolygon.Simplify(pg.OuterRing.ToList(), MapInstance.LocationToViewportPoint, MapInstance.ViewportPointToLocation);

        //        foreach (var s in simples)
        //        {
        //            var edges = s.Splice(wrapPoint);

        //            var leftpolygons = SimplePolygon.FindCircuits(edges.Item1);
        //            var rightpolygons = SimplePolygon.FindCircuits(edges.Item2);

        //            List<SimplePolygon> all = new List<SimplePolygon>();

        //            if (leftpolygons != null)
        //                all = all.Concat(leftpolygons).ToList();

        //            if (rightpolygons != null)
        //                all = all.Concat(rightpolygons).ToList();

        //            foreach (var poly in all)
        //            {
        //                PolygonGeometry temp = new PolygonGeometry();
        //                temp.OuterRing = new System.Collections.ObjectModel.ObservableCollection<Location>(poly.GetVertices());
        //                temp.Stroke = pg.Stroke;

        //                DrawPoly(temp);
        //            }
        //        }
        //    }
        //    else
        //    {
        //        DrawPoly(pg);
        //    }
        //}

        //void DrawPoly(PolygonGeometry pg)
        //{
        //    int[] points = GetPoints(pg);

        //    // ensure first and last points
        //    int[] firstpoint = new int[2] { points[0], points[1] };

        //    int pointsLength = points.Length;

        //    if (points[pointsLength - 2] != firstpoint[0] && points[pointsLength - 1] != firstpoint[1])
        //    {
        //        points = points.Concat(firstpoint).ToArray();
        //    }

        //    SolidColorBrush scb = pg.Stroke as SolidColorBrush;
        //    Color color = scb.Color;

        //    BitmapTarget.FillPolygon(points, color);
        //}

    }
}

